package com.neusoft.utils.mybatis;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import jodd.cache.Cache;
import jodd.cache.LRUCache;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.TokenMgrError;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.TablesNamesFinder;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.log4j.Logger;

import com.neusoft.utils.config.SystemConfig;
import com.neusoft.utils.encrypt.MD5Util;

@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class ShowSqlInterceptor implements Interceptor {
	protected final static Logger logger = Logger.getLogger(ShowSqlInterceptor.class);
	@SuppressWarnings("unused")
	private Properties properties;
	public static Cache<String, Cache<String, Object>> dbCacheMap = new LRUCache<String, Cache<String, Object>>(0);
	public static Map<String, List<String>> tableCache = new HashMap<String, List<String>>();
	public static boolean useCache = false;

	public Object intercept(Invocation invocation) throws Throwable {
		MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
		Object parameter = null;
		if (invocation.getArgs().length > 1) {
			parameter = invocation.getArgs()[1];
		}
		String sqlId = mappedStatement.getId();
		BoundSql boundSql = mappedStatement.getBoundSql(parameter);
		Configuration configuration = mappedStatement.getConfiguration();
		String orgSql = getOrgSql(boundSql);
		String sql = getSql(configuration, boundSql, orgSql);
		// cache
		String lowerOrgSql = orgSql.toLowerCase();
		String lowerSql = sql.toLowerCase();
		Cache<String, Object> valueMap = null;
		if (lowerSql.startsWith("select") && useCache) {
			valueMap = dbCacheMap.get(MD5Util.getMD5(lowerOrgSql));
			if (null != valueMap) {
				logger.debug("\nputKey:" + MD5Util.getMD5(lowerSql));
				Object resultValue = valueMap.get(MD5Util.getMD5(lowerSql));
				if (null != resultValue) {
					logger.debug("\n" + sql);
					logger.debug("\n==from cache===");
					return resultValue;
				}
			}
		}
		Object returnValue = null;
		long start = System.currentTimeMillis();
		returnValue = invocation.proceed();
		if (useCache) {
			clear(lowerOrgSql,orgSql);
			if (lowerSql.startsWith("select") &&canCache(lowerOrgSql)&&getTables(orgSql)) {
				if (null == valueMap) {
					valueMap = new LRUCache<String, Object>(SystemConfig.getIntProperty("cache.table.size"));
					dbCacheMap.put(MD5Util.getMD5(lowerOrgSql), valueMap);
				}
				valueMap.put(MD5Util.getMD5(lowerSql), returnValue);
			}
		}
		long end = System.currentTimeMillis();
		long time = (end - start);
		if (time > 1) {
			String printSql = printSql(sql, sqlId, time);
			logger.debug(printSql);
		}
		return returnValue;
	}

	public static void clearTable(String tableName) {
		if (null != tableCache.get(tableName)) {
			List<String> keyList = tableCache.get(tableName);
			for (String key : keyList) {
				logger.debug("\nremoveKey:" + key);
				dbCacheMap.remove(key);
			}
			tableCache.put(tableName, null);
			logger.debug("\n" + tableName + "\n==clear cache==");
		}
	}

	private void clear(String lowerUpdateSql,String updateSql) {
		if (lowerUpdateSql.startsWith("update")) {
			String[] updateSqls = updateSql.split(";");
			for (String updateSqlx : updateSqls) {
				try {
					Statement stmt = CCJSqlParserUtil.parse(updateSqlx);
					Update selectStatement = (Update) stmt;
					TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
					List<String> tableList = tablesNamesFinder.getTableList(selectStatement);
					for (String table : tableList) {
						clearTable(table);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		if (lowerUpdateSql.startsWith("insert")) {
			String[] updateSqls = updateSql.split(";");
			for (String updateSqlx : updateSqls) {
				try {
					Statement stmt = CCJSqlParserUtil.parse(updateSqlx);
					Insert selectStatement = (Insert) stmt;
					TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
					List<String> tableList = tablesNamesFinder.getTableList(selectStatement);
					for (String table : tableList) {
						clearTable(table);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}

	}
    private boolean canCache(String lowerSql){
    	if(lowerSql.indexOf("f_")!=-1){
    		return false;
    	}
    	if(lowerSql.indexOf("view_")!=-1){
    		return false;
    	}
    	return true;
    }
	private boolean getTables(String orgSql) {
		try {
			Statement stmt = CCJSqlParserUtil.parse(orgSql);
			Select selectStatement = (Select) stmt;
			TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
			List<String> tableList = tablesNamesFinder.getTableList(selectStatement);
			for (String table : tableList) {
				if (null == tableCache.get(table)) {
					tableCache.put(table, new ArrayList<String>());
				}
				List<String> keyList = tableCache.get(table);
				keyList.add(MD5Util.getMD5(orgSql.toLowerCase()));
			}
		} catch (TokenMgrError e) {	
			//logger.debug(ExceptionUtils.getStackTrace(e));
			return false;
		} catch (JSQLParserException e) {
			//logger.debug(ExceptionUtils.getStackTrace(e));
			return false;
		}
		return true;
	}

	public static String getSql(Configuration configuration, BoundSql boundSql, String orgSql) {
		String sql = showSql(configuration, boundSql, orgSql);
		return sql;
	}

	public static String printSql(String sql, String sqlId, long time) {
		StringBuilder str = new StringBuilder(100);
		str.append("\n");
		str.append(sqlId);
		str.append("\n");
		str.append(sql);// SQLFormatter.format(sql)
		str.append("\n用时:");
		str.append(time);
		str.append("ms");
		return str.toString();
	}

	private static String getParameterValue(Object obj) {
		String value = null;
		if (obj instanceof String) {
			value = "'" + obj.toString() + "'";
		} else if (obj instanceof Date) {
			DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
			value = "'" + formatter.format(new Date()) + "'";
		} else {
			if (obj != null) {
				value = obj.toString();
			} else {
				value = "";
			}

		}
		return value;
	}

	public static String getOrgSql(BoundSql boundSql) {
		String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
		return sql;
	}

	public static String showSql(Configuration configuration, BoundSql boundSql, String orgSql) {
		Object parameterObject = boundSql.getParameterObject();
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		String sql = orgSql;
		if (parameterMappings.size() > 0 && parameterObject != null) {
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
				sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

			} else {
				MetaObject metaObject = configuration.newMetaObject(parameterObject);
				for (ParameterMapping parameterMapping : parameterMappings) {
					String propertyName = parameterMapping.getProperty();
					if (metaObject.hasGetter(propertyName)) {
						Object obj = metaObject.getValue(propertyName);
						sql = sql.replaceFirst("\\?", getParameterValue(obj));
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						Object obj = boundSql.getAdditionalParameter(propertyName);
						sql = sql.replaceFirst("\\?", getParameterValue(obj));
					}
				}
			}
		}
		return sql;
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties0) {
		this.properties = properties0;
	}
}